/**
 * Copyright 2012 Google Inc. All Rights Reserved.
 */
package com.google.appengine;

import static java.io.File.separator;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.repository.RemoteRepository;

import com.google.appengine.tools.development.DevAppServerMain;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;

/**
 * Runs the App Engine development server.
 *
 * @goal devserver
 * @execute phase="package"
 */
public class DevAppServerRunner extends AbstractMojo {

  /**
   * The entry point to Aether, i.e. the component doing all the work.
   *
   * @component
   */
  private RepositorySystem repoSystem;

  /**
   * The current repository/network configuration of Maven.
   *
   * @parameter default-value="${repositorySystemSession}"
   * @readonly
   */
  private RepositorySystemSession repoSession;

  /**
   * The project's remote repositories to use for the resolution of project dependencies.
   *
   * @parameter default-value="${project.remoteProjectRepositories}"
   * @readonly
   */
  private List<RemoteRepository> projectRepos;

  /**
   * The project's remote repositories to use for the resolution of plugins and their dependencies.
   *
   * @parameter default-value="${project.remotePluginRepositories}"
   * @readonly
   */
  private List<RemoteRepository> pluginRepos;

  /**
   * @parameter expression="${project}"
   * @required
   * @readonly
   */
  private MavenProject project;

  /**
   * The server to use to determine the latest SDK version.
   *
   * @parameter
   */
  private String server;

  /**
   * The address of the interface on the local machine to bind to (or 0.0.0.0 for all interfaces).
   *
   * @parameter
   */
  private String address;

  /**
   * The port number to bind to on the local machine.
   *
   * @parameter
   */
  private String port;

  /**
   * Disable the check for newer SDK version.
   *
   * @parameter
   */
  private boolean disableUpdateCheck;

  /**
   * Whether or not to run in the background;
   * 
   * @parameter
   */
  private boolean daemon;
  
  /**
   * Additional flags to the JVM used to run the dev server.
   *
   * @parameter
   */
  private List<String> jvmFlags;

  public void execute() throws MojoExecutionException, MojoFailureException {
    long id = Thread.currentThread().getId();
    
    boolean foundServer = false;
    int numTries = 10;
    
    try {
      while (!foundServer && !Thread.interrupted()) {
        if (numTries-- == 0)
          throw new Exception("timed out");

        // get preferences
        try (DevAppServerPreferences prefs = new DevAppServerPreferences()) {
          int port = prefs.getPort();
          if (port == -1) {
            getLog().info("No controller port set");
            startController();
            Thread.sleep(1000);
            continue;
          }

          // try to connect to controller
          try (DevAppServerClient client = new DevAppServerClient(port)) {
            if (!client.isAlive()) {
              startController();
              Thread.sleep(1000);
              continue;
            } else if (!client.isServing(id)) {
              startServer(client, id);
              if (!daemon)
                client.close();
            }
          }
        }
      }
    } catch (Exception e) {
      throw new MojoFailureException("Error running Google App Engine Development Server", e);
    }    
  }

  private void startController() throws IOException {
    List<String> command = new ArrayList<String>();

    command.add(joinOnFileSeparator(System.getProperty("java.home"), "bin", "java"));
    command.add("-cp");
    command.add(System.getProperty("java.class.path", "."));
    command.add(DevAppServerController.class.getCanonicalName());

    getLog().info("Running " + Joiner.on(" ").join(command));
    ProcessBuilder processBuilder = new ProcessBuilder(command);
    processBuilder.redirectErrorStream(true);
    final Process process = processBuilder.start();

    Thread stdOutThread = null;
    stdOutThread = new Thread("standard-out-redirection-devappserver") {
      @Override
      public void run() {
        try (Scanner stdOut = new Scanner(process.getInputStream())) {
          while (stdOut.hasNextLine() && !Thread.interrupted()) {
            getLog().info(stdOut.nextLine());
          }
        }
      }
    };
    stdOutThread.start();

    Thread stdErrThread = null;
    stdErrThread = new Thread("standard-err-redirection-devappserver") {
      @Override
      public void run() {
        try (Scanner stdErr = new Scanner(process.getErrorStream())) {
          while (stdErr.hasNextLine() && !Thread.interrupted()) {
            getLog().error(stdErr.nextLine());
          }
        }
      }
    };
    stdErrThread.start();
  }
  
  private void startServer(DevAppServerClient client, long id) throws Exception {
    File sdkBaseDir = SdkResolver.getSdk(project, repoSystem, repoSession, pluginRepos, projectRepos);
    String appDir = project.getBuild().getDirectory() + "/" + project.getBuild().getFinalName();
    String javaExecutable = joinOnFileSeparator(System.getProperty("java.home"), "bin", "java");

    List<String> devAppServerCommand = new ArrayList<String>();
    devAppServerCommand.add(javaExecutable);

    if (System.getProperty("os.name").equalsIgnoreCase("Mac OS X")) {
      devAppServerCommand.add("-XstartOnFirstThread");
    }

    // Add in the appengine agent
    String appengineAgentJar = new File(sdkBaseDir, joinOnFileSeparator("lib", "agent", "appengine-agent.jar")).getAbsolutePath();
    devAppServerCommand.add("-javaagent:" + appengineAgentJar);

    // Setup the overrides jar for jdk classes
    String appengineDevJdkOverridesJar = new File(sdkBaseDir, joinOnFileSeparator("lib", "override", "appengine-dev-jdk-overrides.jar")).getAbsolutePath();
    devAppServerCommand.add("-Xbootclasspath/p:" + appengineDevJdkOverridesJar);

    // Setup the classpath to point to the tools jar
    String appengineToolsApiJar = new File(sdkBaseDir, joinOnFileSeparator("lib", "appengine-tools-api.jar")).getAbsolutePath();
    devAppServerCommand.add("-classpath");
    devAppServerCommand.add(appengineToolsApiJar);

    // Add jvm flags
    if(jvmFlags != null && !jvmFlags.isEmpty()) {
      devAppServerCommand.addAll(jvmFlags);
    }

    // Point to the DevAppServerMain class
    devAppServerCommand.add(DevAppServerMain.class.getCanonicalName());

    // Add in additional options for starting the DevAppServer
    if(server != null) {
      devAppServerCommand.add("-s");
      devAppServerCommand.add(server);
    }

    if(address != null) {
      devAppServerCommand.add("-a");
      devAppServerCommand.add(address);
    }

    if(port != null) {
      devAppServerCommand.add("-p");
      devAppServerCommand.add(port);
    }

    if(disableUpdateCheck) {
      devAppServerCommand.add("--disable_update_check");
    }

    // Point to our application
    devAppServerCommand.add(appDir);
    
    // now ask controller to start
    client.start(id, devAppServerCommand, !daemon, getLog());
  }
    
  private String joinOnFileSeparator(String... pathComponents) {
    return Joiner.on(separator).join(ImmutableList.copyOf(pathComponents));
  }
  
}
